Skip to main content

libobs_wrapper\scenes\scene_item/
mod.rs

1//! Scene items essentially hold the transform information and the source in a scene itself.
2//! They are specific to the scene which they were created in.
3mod traits;
4pub use traits::SceneItemExtSceneTrait;
5
6use std::{
7    fmt::Debug,
8    hash::Hash,
9    sync::{Arc, Mutex},
10};
11
12use libobs::{obs_scene_item, obs_transform_info, obs_video_info};
13use num_traits::FromPrimitive;
14
15use crate::{
16    enums::{
17        ObsAlignment, ObsBlendMethod, ObsBlendMode, ObsBoundsType, ObsOrderMovement, ObsScaleType,
18    },
19    graphics::Vec2,
20    impl_obs_drop,
21    macros::trait_with_optional_send_sync,
22    run_with_obs,
23    runtime::ObsRuntime,
24    scenes::{ObsSceneRef, ObsTransformInfo, ObsTransformInfoBuilder},
25    sources::ObsSourceTrait,
26    unsafe_send::{Sendable, SmartPointerSendable},
27    utils::{ObsDropGuard, ObsError},
28};
29
30/// Pixel crop applied to a scene item before its transform is evaluated.
31///
32/// Values map directly to libobs's left/top/right/bottom scene-item crop. Positive values
33/// remove pixels from the corresponding edge of the underlying source.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct ObsSceneItemCrop {
36    pub left: i32,
37    pub top: i32,
38    pub right: i32,
39    pub bottom: i32,
40}
41
42impl ObsSceneItemCrop {
43    pub const fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self {
44        Self {
45            left,
46            top,
47            right,
48            bottom,
49        }
50    }
51}
52
53impl From<ObsSceneItemCrop> for libobs::obs_sceneitem_crop {
54    fn from(value: ObsSceneItemCrop) -> Self {
55        Self {
56            left: value.left,
57            top: value.top,
58            right: value.right,
59            bottom: value.bottom,
60        }
61    }
62}
63
64impl From<libobs::obs_sceneitem_crop> for ObsSceneItemCrop {
65    fn from(value: libobs::obs_sceneitem_crop) -> Self {
66        Self::new(value.left, value.top, value.right, value.bottom)
67    }
68}
69
70/// Complete render-transform state for one scene item. Unlike [`ObsTransformInfo`], this also
71/// includes edge crop, scaling filter, and blending state stored outside `obs_transform_info`.
72#[derive(Debug, Clone, PartialEq)]
73pub struct ObsSceneItemTransformSnapshot {
74    pub position: Vec2,
75    pub scale: Vec2,
76    pub alignment: ObsAlignment,
77    pub rotation: f32,
78    pub bounds: Vec2,
79    pub bounds_type: ObsBoundsType,
80    pub bounds_alignment: ObsAlignment,
81    pub crop_to_bounds: bool,
82    pub crop: ObsSceneItemCrop,
83    pub scale_filter: ObsScaleType,
84    pub blend_method: ObsBlendMethod,
85    pub blend_mode: ObsBlendMode,
86}
87
88/// Restorable scene-item state including transform, visibility, locking and native z-order.
89#[derive(Debug, Clone, PartialEq)]
90pub struct ObsSceneItemStateSnapshot {
91    pub transform: ObsSceneItemTransformSnapshot,
92    pub visible: bool,
93    pub locked: bool,
94    pub order_position: i32,
95}
96
97#[derive(Debug)]
98pub(super) struct _ObsSceneItemDropGuard {
99    scene_item: Sendable<*mut obs_scene_item>,
100    removed: Arc<Mutex<bool>>,
101    runtime: ObsRuntime,
102}
103
104impl ObsDropGuard for _ObsSceneItemDropGuard {}
105impl_obs_drop!(
106    _ObsSceneItemDropGuard,
107    (scene_item, removed),
108    move || unsafe {
109        // Safety: construction takes an explicit native scene-item reference. Remove the item from
110        // its scene at most once, then release the reference owned by this managed handle.
111        let mut removed = removed
112            .lock()
113            .unwrap_or_else(|poisoned| poisoned.into_inner());
114        if !*removed {
115            libobs::obs_sceneitem_remove(scene_item.0);
116            *removed = true;
117        }
118        libobs::obs_sceneitem_release(scene_item.0);
119    }
120);
121
122/// Wraps one explicit native scene-item reference in the managed lifetime lease used by all
123/// scene-item handle types. The caller must already own one `obs_sceneitem_addref` reference.
124pub(super) fn wrap_owned_scene_item_ref(
125    scene_item: Sendable<*mut obs_scene_item>,
126    runtime: ObsRuntime,
127) -> (SmartPointerSendable<*mut obs_scene_item>, Arc<Mutex<bool>>) {
128    let removed = Arc::new(Mutex::new(false));
129    let drop_guard = _ObsSceneItemDropGuard {
130        scene_item: scene_item.clone(),
131        removed: removed.clone(),
132        runtime: runtime.clone(),
133    };
134    let handle = SmartPointerSendable::new(
135        scene_item.0,
136        Arc::new(drop_guard),
137        runtime.native_registry(),
138    );
139    (handle, removed)
140}
141
142pub(super) fn scene_item_native_handle<T: SceneItemTrait + ?Sized>(
143    item: &T,
144) -> SmartPointerSendable<*mut obs_scene_item> {
145    item.__native_handle().clone()
146}
147
148pub(super) fn mark_scene_item_removed<T: SceneItemTrait + ?Sized>(item: &T) {
149    let mut removed = item
150        .__removed_flag()
151        .lock()
152        .unwrap_or_else(|poisoned| poisoned.into_inner());
153    *removed = true;
154}
155
156#[derive(Debug, Clone)]
157/// Holds the specific source that was added to the scene and its scene item.
158/// If this struct is attached to the scene, it'll not be dropped as the scene
159/// internally stores this struct, thus the source will also not be dropped.
160pub struct ObsSceneItemRef<T: ObsSourceTrait + Clone> {
161    // Drop the scene item first...
162    scene_item_ptr: SmartPointerSendable<*mut obs_scene_item>,
163    removed: Arc<Mutex<bool>>,
164    runtime: ObsRuntime,
165    // Then the scene
166    // Note: Ideally, we'd want to keep the whole ObsScene struct, however
167    // that would lead to a circular dependency, meaning that this SceneItem / the scene
168    // would never be dropped. Because the only argument the scene takes is its name
169    // and there are no settings attached to it, it's safe to only have a SmartPointer
170    // and not the full scene
171    _scene_ptr: SmartPointerSendable<*mut libobs::obs_scene>,
172
173    // And at last the source
174    underlying_source: T,
175}
176
177/// Type-erased managed scene-item handle used when libobs itself replaces a native item, such as
178/// during group ungrouping. Ordinary source insertion still returns [`ObsSceneItemRef<T>`].
179#[derive(Debug, Clone)]
180pub struct ObsSceneItemHandle {
181    scene_item_ptr: SmartPointerSendable<*mut obs_scene_item>,
182    removed: Arc<Mutex<bool>>,
183    runtime: ObsRuntime,
184    _scene_ptr: SmartPointerSendable<*mut libobs::obs_scene_t>,
185}
186
187impl ObsSceneItemHandle {
188    pub(super) fn from_owned_native_ref(
189        item: Sendable<*mut obs_scene_item>,
190        scene_ptr: SmartPointerSendable<*mut libobs::obs_scene_t>,
191        runtime: ObsRuntime,
192    ) -> Self {
193        let (scene_item_ptr, removed) = wrap_owned_scene_item_ref(item, runtime.clone());
194        Self {
195            scene_item_ptr,
196            removed,
197            runtime,
198            _scene_ptr: scene_ptr,
199        }
200    }
201}
202
203impl<T: ObsSourceTrait + Clone> ObsSceneItemRef<T> {
204    pub(crate) fn new(
205        scene: &ObsSceneRef,
206        source: T,
207        runtime: ObsRuntime,
208    ) -> Result<Self, ObsError> {
209        runtime.ensure_same_runtime(source.runtime())?;
210        runtime.ensure_same_runtime(&scene.runtime)?;
211        let scene_ptr = scene.as_ptr();
212        let source_ptr = source.__native_handle();
213
214        let scene_item_ptr = run_with_obs!(runtime, (scene_ptr, source_ptr), move || {
215            let ptr = unsafe {
216                // Safety: The pointers are valid as they are safe pointers
217                libobs::obs_scene_add(scene_ptr.get_ptr(), source_ptr.get_ptr())
218            };
219
220            if ptr.is_null() {
221                Err(ObsError::NullPointer(None))
222            } else {
223                unsafe {
224                    // Safety: `ptr` is a live item returned by obs_scene_add. The managed handle
225                    // owns this added reference until its drop guard releases it.
226                    libobs::obs_sceneitem_addref(ptr);
227                }
228                Ok(Sendable(ptr))
229            }
230        })??;
231
232        let removed = Arc::new(Mutex::new(false));
233        let drop_guard = _ObsSceneItemDropGuard {
234            scene_item: scene_item_ptr.clone(),
235            removed: removed.clone(),
236            runtime: runtime.clone(),
237        };
238
239        let scene_item_ptr = SmartPointerSendable::new(
240            scene_item_ptr.0,
241            Arc::new(drop_guard),
242            runtime.native_registry(),
243        );
244
245        Ok(Self {
246            underlying_source: source,
247            _scene_ptr: scene.as_ptr().clone(),
248            scene_item_ptr,
249            removed,
250            runtime,
251        })
252    }
253}
254
255trait_with_optional_send_sync! {
256    pub(crate) trait SceneItemTraitSealed: Debug {
257        fn __native_handle(&self) -> &SmartPointerSendable<*mut obs_scene_item>;
258        fn __removed_flag(&self) -> &Arc<Mutex<bool>>;
259    }
260}
261
262trait_with_optional_send_sync! {
263    #[allow(private_bounds)]
264    pub trait SceneItemTrait: SceneItemTraitSealed {
265        fn runtime(&self) -> ObsRuntime;
266
267        fn object_id(&self) -> crate::unsafe_send::NativeObjectId {
268            self.__native_handle().native_id()
269        }
270
271        /// Removes this item from its scene immediately while keeping the typed handle valid
272        /// until its final Rust reference is dropped. Repeated calls are harmless.
273        fn remove_from_scene(&self) -> Result<(), ObsError> {
274            let mut removed = self
275                .__removed_flag()
276                .lock()
277                .unwrap_or_else(|poisoned| poisoned.into_inner());
278            if *removed {
279                return Ok(());
280            }
281            let self_ptr = self.__native_handle().clone();
282            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
283                // Safety: the managed item owns a native reference, so removal cannot invalidate
284                // the pointer until the drop guard later releases that reference.
285                libobs::obs_sceneitem_remove(self_ptr.get_ptr());
286            })?;
287            *removed = true;
288            Ok(())
289        }
290
291        /// Whether this item has been explicitly removed from its scene.
292        fn is_removed(&self) -> bool {
293            *self
294                .__removed_flag()
295                .lock()
296                .unwrap_or_else(|poisoned| poisoned.into_inner())
297        }
298
299        /// Gets the transform info of the given source in this scene.
300        fn get_transform_info(&self) -> Result<ObsTransformInfo, ObsError> {
301            let self_ptr = self.__native_handle().clone();
302            let item_info = run_with_obs!(self.runtime(), (self_ptr), move || {
303                let mut item_info: obs_transform_info = unsafe {
304                    // Safety: this is safe to call because we are filling a struct with zeros
305                    std::mem::zeroed()
306                };
307                unsafe {
308                    // Safety: Fill the transform info struct with the data
309                    libobs::obs_sceneitem_get_info2(self_ptr.get_ptr(), &mut item_info)
310                };
311
312                ObsTransformInfo(item_info)
313            })?;
314
315            Ok(item_info)
316        }
317
318        /// Gets the position of the given source in this scene.
319        fn get_source_position(&self) -> Result<Vec2, ObsError> {
320            let self_ptr = self.__native_handle().clone();
321            let position = run_with_obs!(self.runtime(), (self_ptr), move || {
322                let main_pos = unsafe {
323                    // Safety: this is safe to call because we a filling a struct with zeros
324                    let mut main_pos: libobs::vec2 = std::mem::zeroed();
325
326                    // Safety: Fill the vec2 struct with the position data
327                    libobs::obs_sceneitem_get_pos(self_ptr.get_ptr(), &mut main_pos);
328
329                    main_pos
330                };
331
332                Vec2::from(main_pos)
333            })?;
334
335            Ok(position)
336        }
337
338        /// Gets the scale of the given source in this scene.
339        fn get_source_scale(&self) -> Result<Vec2, ObsError> {
340            let self_ptr = self.__native_handle().clone();
341            let scale = run_with_obs!(self.runtime(), (self_ptr), move || {
342                let main_pos = unsafe {
343                    // Safety: this is safe to call because we a filling a struct with zeros
344                    let mut main_pos: libobs::vec2 = std::mem::zeroed();
345
346                    // Safety: Fill the vec2 struct with the scale data, this using the correct size
347                    libobs::obs_sceneitem_get_scale(self_ptr.get_ptr(), &mut main_pos);
348
349                    main_pos
350                };
351
352                Vec2::from(main_pos)
353            })?;
354
355            Ok(scale)
356        }
357
358        /// Sets the position of the given source in this scene.
359        fn set_source_position(&self, position: Vec2) -> Result<(), ObsError> {
360            let self_ptr = self.__native_handle().clone();
361
362            run_with_obs!(self.runtime(), (self_ptr), move || {
363                let position: libobs::vec2 = position.into();
364
365                unsafe {
366                    // Safety: The pointer is valid as it is a safe pointer
367                    libobs::obs_sceneitem_set_pos(self_ptr.get_ptr(), &position);
368                }
369            })?;
370
371            Ok(())
372        }
373
374        /// Sets the transform info of the given source in this scene.
375        /// The `ObsTransformInfo` can be built by using the `ObsTransformInfoBuilder`.
376        fn set_transform_info(&self, info: &ObsTransformInfo) -> Result<(), ObsError> {
377            let item_info = Sendable(info.clone());
378            let self_ptr = self.__native_handle().clone();
379
380            run_with_obs!(self.runtime(), (self_ptr, item_info), move || {
381                let item_info = item_info.0 .0;
382
383                unsafe {
384                    // Safety: The pointers are valid as they are safe pointers
385                    libobs::obs_sceneitem_set_info2(self_ptr.get_ptr(), &item_info);
386                }
387            })?;
388
389            Ok(())
390        }
391
392        /// Fits the given source to the screen size.
393        /// If the source is locked, no action is taken.
394        ///
395        /// Returns `Ok(true)` if the source was resized, `Ok(false)` if the source was locked and not resized.
396        fn fit_source_to_screen(&self) -> Result<bool, ObsError> {
397            let self_ptr = self.__native_handle().clone();
398            let is_locked = {
399                run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
400                    // Safety: The pointer is valid as it is a safe pointer
401                    libobs::obs_sceneitem_locked(self_ptr.get_ptr())
402                })?
403            };
404
405            if is_locked {
406                return Ok(false);
407            }
408
409            let ovi = run_with_obs!(self.runtime(), (), move || {
410                let mut ovi = std::mem::MaybeUninit::<obs_video_info>::uninit();
411                let success = unsafe {
412                    // Safety: This is safe because we are providing a valid pointer to be filled
413                    libobs::obs_get_video_info(ovi.as_mut_ptr())
414                };
415
416                if success {
417                    let res = unsafe {
418                        // Safety: This is safe because libobs filled the pointer and returned success
419                        ovi.assume_init()
420                    };
421
422                    Ok(Sendable(res))
423                } else {
424                    Err(ObsError::NullPointer(Some(
425                        "Failed to get video info".to_string(),
426                    )))
427                }
428            })??;
429
430            let bounds_crop = run_with_obs!(self.runtime(), (self_ptr), move || {
431                unsafe {
432                    // Safety: The pointer is valid as it is a safe pointer
433                    libobs::obs_sceneitem_get_bounds_crop(self_ptr.get_ptr())
434                }
435            })?;
436
437            // We are not constructing it from the source here because we want to reset full transform (so we use build instead of build_with_fallback)
438            let item_info = ObsTransformInfoBuilder::new()
439                .set_bounds_type(ObsBoundsType::ScaleInner)
440                .set_crop_to_bounds(bounds_crop)
441                .build(ovi.0.base_width, ovi.0.base_height);
442
443            self.set_transform_info(&item_info)?;
444            Ok(true)
445        }
446
447        /// Returns whether the item is visible.
448        fn is_visible(&self) -> Result<bool, ObsError> {
449            let self_ptr = self.__native_handle().clone();
450            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
451                // Safety: the managed item retains a native reference for the actor call.
452                libobs::obs_sceneitem_visible(self_ptr.get_ptr())
453            })
454        }
455
456        /// Returns the explicit pixel crop applied to this scene item.
457        fn crop(&self) -> Result<ObsSceneItemCrop, ObsError> {
458            let self_ptr = self.__native_handle().clone();
459            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
460                // Safety: the managed item retains a native reference for the actor call. The
461                // output structure is initialized even if libobs unexpectedly declines to write.
462                let mut crop = libobs::obs_sceneitem_crop {
463                    left: 0,
464                    top: 0,
465                    right: 0,
466                    bottom: 0,
467                };
468                libobs::obs_sceneitem_get_crop(self_ptr.get_ptr(), &mut crop);
469                ObsSceneItemCrop::from(crop)
470            })
471        }
472
473        /// Replaces the explicit left/top/right/bottom pixel crop for this scene item.
474        /// Negative edge values are clamped to zero by libobs.
475        fn set_crop(&self, crop: ObsSceneItemCrop) -> Result<(), ObsError> {
476            let self_ptr = self.__native_handle().clone();
477            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
478                // Safety: the managed item retains a native reference for the actor call and the
479                // converted crop value lives for the complete native setter invocation.
480                let crop: libobs::obs_sceneitem_crop = crop.into();
481                libobs::obs_sceneitem_set_crop(self_ptr.get_ptr(), &crop);
482            })
483        }
484
485        /// Shows or hides the item.
486        fn set_visible(&self, visible: bool) -> Result<(), ObsError> {
487            let self_ptr = self.__native_handle().clone();
488            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
489                // Safety: the managed item retains a native reference for the actor call.
490                let _ = libobs::obs_sceneitem_set_visible(self_ptr.get_ptr(), visible);
491            })
492        }
493
494        /// Returns whether the item transform is locked.
495        fn is_locked(&self) -> Result<bool, ObsError> {
496            let self_ptr = self.__native_handle().clone();
497            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
498                // Safety: the managed item retains a native reference for the actor call.
499                libobs::obs_sceneitem_locked(self_ptr.get_ptr())
500            })
501        }
502
503        /// Locks or unlocks the item transform.
504        fn set_locked(&self, locked: bool) -> Result<(), ObsError> {
505            let self_ptr = self.__native_handle().clone();
506            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
507                // Safety: the managed item retains a native reference for the actor call.
508                let _ = libobs::obs_sceneitem_set_locked(self_ptr.get_ptr(), locked);
509            })
510        }
511
512        /// Returns the clockwise rotation in degrees.
513        fn rotation(&self) -> Result<f32, ObsError> {
514            let self_ptr = self.__native_handle().clone();
515            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
516                // Safety: the managed item retains a native reference for the actor call.
517                libobs::obs_sceneitem_get_rot(self_ptr.get_ptr())
518            })
519        }
520
521        /// Sets the clockwise rotation in degrees.
522        fn set_rotation(&self, degrees: f32) -> Result<(), ObsError> {
523            let self_ptr = self.__native_handle().clone();
524            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
525                // Safety: the managed item retains a native reference for the actor call.
526                libobs::obs_sceneitem_set_rot(self_ptr.get_ptr(), degrees);
527            })
528        }
529
530        /// Returns the item's zero-based order position within its scene.
531        fn order_position(&self) -> Result<i32, ObsError> {
532            let self_ptr = self.__native_handle().clone();
533            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
534                // Safety: the managed item retains a native reference for the actor call.
535                libobs::obs_sceneitem_get_order_position(self_ptr.get_ptr())
536            })
537        }
538
539        /// Moves the item to an absolute order position within its scene.
540        fn set_order_position(&self, position: i32) -> Result<(), ObsError> {
541            if position < 0 {
542                return Err(ObsError::InvalidOperation(
543                    "Scene item order position cannot be negative".to_string(),
544                ));
545            }
546            let self_ptr = self.__native_handle().clone();
547            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
548                // Safety: the managed item retains a native reference for the actor call.
549                libobs::obs_sceneitem_set_order_position(self_ptr.get_ptr(), position);
550            })
551        }
552
553        /// Moves the item relative to the other items in its scene.
554        fn move_order(&self, movement: ObsOrderMovement) -> Result<(), ObsError> {
555            let self_ptr = self.__native_handle().clone();
556            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
557                // Safety: the managed item retains a native reference for the actor call.
558                libobs::obs_sceneitem_set_order(self_ptr.get_ptr(), movement as libobs::obs_order_movement);
559            })
560        }
561
562        /// Returns the scaling filter used when this item is resized.
563        fn scale_filter(&self) -> Result<ObsScaleType, ObsError> {
564            let self_ptr = self.__native_handle().clone();
565            let raw = run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
566                // Safety: the managed scene-item reference remains alive for the actor call.
567                libobs::obs_sceneitem_get_scale_filter(self_ptr.get_ptr())
568            })?;
569            ObsScaleType::from_i64(raw as i64).ok_or_else(|| {
570                ObsError::EnumConversionError(format!("unknown scene-item scale filter {raw}"))
571            })
572        }
573
574        fn set_scale_filter(&self, filter: ObsScaleType) -> Result<(), ObsError> {
575            let self_ptr = self.__native_handle().clone();
576            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
577                // Safety: the managed item remains alive and `filter` is a libobs enum value.
578                libobs::obs_sceneitem_set_scale_filter(
579                    self_ptr.get_ptr(),
580                    filter as libobs::obs_scale_type,
581                );
582            })
583        }
584
585        fn blend_method(&self) -> Result<ObsBlendMethod, ObsError> {
586            let self_ptr = self.__native_handle().clone();
587            let raw = run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
588                // Safety: the managed scene-item reference remains alive for the actor call.
589                libobs::obs_sceneitem_get_blending_method(self_ptr.get_ptr())
590            })?;
591            ObsBlendMethod::from_i64(raw as i64).ok_or_else(|| {
592                ObsError::EnumConversionError(format!("unknown scene-item blend method {raw}"))
593            })
594        }
595
596        fn set_blend_method(&self, method: ObsBlendMethod) -> Result<(), ObsError> {
597            let self_ptr = self.__native_handle().clone();
598            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
599                // Safety: the managed item remains alive and `method` is a libobs enum value.
600                libobs::obs_sceneitem_set_blending_method(
601                    self_ptr.get_ptr(),
602                    method as libobs::obs_blending_method,
603                );
604            })
605        }
606
607        fn blend_mode(&self) -> Result<ObsBlendMode, ObsError> {
608            let self_ptr = self.__native_handle().clone();
609            let raw = run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
610                // Safety: the managed scene-item reference remains alive for the actor call.
611                libobs::obs_sceneitem_get_blending_mode(self_ptr.get_ptr())
612            })?;
613            ObsBlendMode::from_i64(raw as i64).ok_or_else(|| {
614                ObsError::EnumConversionError(format!("unknown scene-item blend mode {raw}"))
615            })
616        }
617
618        fn set_blend_mode(&self, mode: ObsBlendMode) -> Result<(), ObsError> {
619            let self_ptr = self.__native_handle().clone();
620            run_with_obs!(self.runtime(), (self_ptr), move || unsafe {
621                // Safety: the managed item remains alive and `mode` is a libobs enum value.
622                libobs::obs_sceneitem_set_blending_mode(
623                    self_ptr.get_ptr(),
624                    mode as libobs::obs_blending_type,
625                );
626            })
627        }
628
629        /// Captures every transform-affecting scene-item property exposed by libobs.
630        fn transform_snapshot(&self) -> Result<ObsSceneItemTransformSnapshot, ObsError> {
631            let info = self.get_transform_info()?;
632            let bounds_type = info.get_bounds_type().ok_or_else(|| {
633                ObsError::EnumConversionError("unknown scene-item bounds type".into())
634            })?;
635            Ok(ObsSceneItemTransformSnapshot {
636                position: info.get_pos(),
637                scale: info.get_scale(),
638                alignment: ObsAlignment::from_bits_retain(info.get_alignment()),
639                rotation: info.get_rot(),
640                bounds: info.get_bounds(),
641                bounds_type,
642                bounds_alignment: ObsAlignment::from_bits_retain(info.get_bounds_alignment()),
643                crop_to_bounds: info.get_crop_to_bounds(),
644                crop: self.crop()?,
645                scale_filter: self.scale_filter()?,
646                blend_method: self.blend_method()?,
647                blend_mode: self.blend_mode()?,
648            })
649        }
650
651        /// Restores a transform snapshot through the same managed actor seam as individual setters.
652        fn apply_transform_snapshot(
653            &self,
654            snapshot: &ObsSceneItemTransformSnapshot,
655        ) -> Result<(), ObsError> {
656            let info = ObsTransformInfoBuilder::new()
657                .set_pos(snapshot.position)
658                .set_scale(snapshot.scale)
659                .set_alignment(snapshot.alignment)
660                .set_rot(snapshot.rotation)
661                .set_bounds(snapshot.bounds)
662                .set_bounds_type(snapshot.bounds_type)
663                .set_bounds_alignment(snapshot.bounds_alignment)
664                .set_crop_to_bounds(snapshot.crop_to_bounds)
665                .build_with_fallback(self)?;
666            self.set_transform_info(&info)?;
667            self.set_crop(snapshot.crop)?;
668            self.set_scale_filter(snapshot.scale_filter)?;
669            self.set_blend_method(snapshot.blend_method)?;
670            self.set_blend_mode(snapshot.blend_mode)
671        }
672
673        fn state_snapshot(&self) -> Result<ObsSceneItemStateSnapshot, ObsError> {
674            Ok(ObsSceneItemStateSnapshot {
675                transform: self.transform_snapshot()?,
676                visible: self.is_visible()?,
677                locked: self.is_locked()?,
678                order_position: self.order_position()?,
679            })
680        }
681
682        fn apply_state_snapshot(
683            &self,
684            snapshot: &ObsSceneItemStateSnapshot,
685        ) -> Result<(), ObsError> {
686            self.apply_transform_snapshot(&snapshot.transform)?;
687            self.set_visible(snapshot.visible)?;
688            self.set_locked(snapshot.locked)?;
689            self.set_order_position(snapshot.order_position)
690        }
691
692        /// Preferred shorthand for [`SceneItemTrait::get_source_position`].
693        fn position(&self) -> Result<Vec2, ObsError> {
694            self.get_source_position()
695        }
696
697        /// Preferred shorthand for [`SceneItemTrait::set_source_position`].
698        fn set_position(&self, position: Vec2) -> Result<(), ObsError> {
699            self.set_source_position(position)
700        }
701
702        /// Preferred shorthand for [`SceneItemTrait::get_source_scale`].
703        fn scale(&self) -> Result<Vec2, ObsError> {
704            self.get_source_scale()
705        }
706
707        /// Preferred shorthand for [`SceneItemTrait::set_source_scale`].
708        fn set_scale(&self, scale: Vec2) -> Result<(), ObsError> {
709            self.set_source_scale(scale)
710        }
711
712        /// Sets the scale of the given source in this scene.
713        fn set_source_scale(&self, scale: Vec2) -> Result<(), ObsError> {
714            let self_ptr = self.__native_handle().clone();
715
716            run_with_obs!(self.runtime(), (self_ptr), move || {
717                let scale: libobs::vec2 = scale.into();
718
719                unsafe {
720                    // Safety: The pointer is valid as it is a safe pointer
721                    libobs::obs_sceneitem_set_scale(self_ptr.get_ptr(), &scale);
722                }
723            })?;
724
725            Ok(())
726        }
727    }
728}
729
730impl SceneItemTraitSealed for ObsSceneItemHandle {
731    fn __native_handle(&self) -> &SmartPointerSendable<*mut obs_scene_item> {
732        &self.scene_item_ptr
733    }
734
735    fn __removed_flag(&self) -> &Arc<Mutex<bool>> {
736        &self.removed
737    }
738}
739
740impl SceneItemTrait for ObsSceneItemHandle {
741    fn runtime(&self) -> ObsRuntime {
742        self.runtime.clone()
743    }
744}
745
746impl<T: ObsSourceTrait + Clone> SceneItemTraitSealed for ObsSceneItemRef<T> {
747    fn __native_handle(&self) -> &SmartPointerSendable<*mut obs_scene_item> {
748        &self.scene_item_ptr
749    }
750
751    fn __removed_flag(&self) -> &Arc<Mutex<bool>> {
752        &self.removed
753    }
754}
755
756impl<T: ObsSourceTrait + Clone> SceneItemTrait for ObsSceneItemRef<T> {
757    fn runtime(&self) -> ObsRuntime {
758        self.runtime.clone()
759    }
760}
761
762impl<T> ObsSceneItemRef<T>
763where
764    T: ObsSourceTrait + Clone,
765{
766    /// Returns a reference to the specific source type.
767    pub fn inner_source(&self) -> &T {
768        &self.underlying_source
769    }
770
771    /// Returns a reference to the specific source type.
772    pub fn inner_source_mut(&mut self) -> &mut T {
773        &mut self.underlying_source
774    }
775}
776
777// The macro doesn't support generics yet, so we implement it manually
778//impl_eq_of_ptr!(SceneItemRef<T>, scene_item_ptr);
779
780impl<T: ObsSourceTrait + Clone> PartialEq for ObsSceneItemRef<T> {
781    fn eq(&self, other: &Self) -> bool {
782        self.scene_item_ptr.native_id() == other.scene_item_ptr.native_id()
783    }
784}
785
786impl<T: ObsSourceTrait + Clone> Eq for ObsSceneItemRef<T> {}
787
788impl<T: ObsSourceTrait + Clone> Hash for ObsSceneItemRef<T> {
789    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
790        self.scene_item_ptr.native_id().hash(state);
791    }
792}
793
794#[cfg(test)]
795mod crop_tests {
796    use super::ObsSceneItemCrop;
797
798    #[test]
799    fn obs_scene_item_crop_round_trips_native_layout() {
800        let crop = ObsSceneItemCrop::new(1, 2, 3, 4);
801        let native: libobs::obs_sceneitem_crop = crop.into();
802        assert_eq!(native.left, 1);
803        assert_eq!(native.top, 2);
804        assert_eq!(native.right, 3);
805        assert_eq!(native.bottom, 4);
806        assert_eq!(ObsSceneItemCrop::from(native), crop);
807    }
808
809    #[test]
810    fn obs_scene_item_crop_default_is_uncropped() {
811        assert_eq!(
812            ObsSceneItemCrop::default(),
813            ObsSceneItemCrop::new(0, 0, 0, 0)
814        );
815    }
816}